{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import torch\n",
    "from copy import deepcopy\n",
    "import numpy as np\n",
    "import xarray as xr\n",
    "import pandas as pd\n",
    "import torch.nn as nn\n",
    "import random\n",
    "from tqdm import tqdm\n",
    "from sklearn.model_selection import train_test_split\n",
    "from torch.utils.data import Dataset, DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "#设置种子\n",
    "def set_seed(seed = 427):\n",
    "    random.seed(seed)\n",
    "    np.random.seed(seed)\n",
    "    os.environ['PYTHONHASHSEED'] = str(seed)\n",
    "    torch.manual_seed(seed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def split_month(array,size):#input shape: :,36,24,72\n",
    "    temp=array[:size,0:12,:,:]\n",
    "    temp=temp.reshape(size*12,24,72)\n",
    "    temp2=np.array([temp[i:i+12,:,:] for i in range(size*12-40)])\n",
    "    return temp2\n",
    "\n",
    "def split_month_label(array,size):#input shape: :,24\n",
    "    temp=array[:size,0:12]\n",
    "    temp=temp.reshape(size*12)\n",
    "    temp2=np.array([temp[i+12:i+36] for i in range(size*12-40)])\n",
    "    return temp2\n",
    "    \n",
    "def load_data2():\n",
    "    # CMIP data    \n",
    "    size1=1000\n",
    "    train = xr.open_dataset('../tcdata/enso_round1_train_20210201/CMIP_train.nc')\n",
    "    label = xr.open_dataset('../tcdata/enso_round1_train_20210201/CMIP_label.nc')    \n",
    "    train_sst = train['sst'].values\n",
    "    train_sst= np.concatenate((train_sst[:151*5],train_sst[151*9:151*12],train_sst[151*13:]))  \n",
    "    train_sst=split_month(train_sst,size1)\n",
    "    train_t300 = train['t300'].values\n",
    "    train_t300= np.concatenate((train_t300[:151*5],train_t300[151*9:151*12],train_t300[151*13:]))\n",
    "    train_t300=split_month(train_t300,size1)\n",
    "    train_ua = train['ua'].values\n",
    "    train_ua= np.concatenate((train_ua[:151*5],train_ua[151*9:151*12],train_ua[151*13:])) \n",
    "    train_ua=split_month(train_ua,size1)\n",
    "    train_va = train['va'].values\n",
    "    train_va= np.concatenate((train_va[:151*5],train_va[151*9:151*12],train_va[151*13:]))\n",
    "    train_va=split_month(train_va,size1)\n",
    "    train_label = label['nino'].values\n",
    "    train_label= np.concatenate((train_label[:151*5],train_label[151*9:151*12],train_label[151*13:]))\n",
    "    train_label=split_month_label(train_label,size1)\n",
    "    \n",
    "    #train_ua = np.nan_to_num(train_ua)#缺失值补0\n",
    "    #train_va = np.nan_to_num(train_va)\n",
    "    #train_t300 = np.nan_to_num(train_t300)\n",
    "    #train_sst = np.nan_to_num(train_sst)\n",
    "\n",
    "    # SODA data  \n",
    "    size2=100\n",
    "    train2 = xr.open_dataset('../tcdata/enso_round1_train_20210201/SODA_train.nc')\n",
    "    label2 = xr.open_dataset('../tcdata/enso_round1_train_20210201/SODA_label.nc')\n",
    "    \n",
    "    train_sst2 = train2['sst'].values  # (3890, 12, 24, 72)\n",
    "    train_sst2=split_month(train_sst2,size2)\n",
    "    train_t3002 = train2['t300'].values\n",
    "    train_t3002=split_month(train_t3002,size2)\n",
    "    train_ua2 = train2['ua'].values\n",
    "    train_ua2=split_month(train_ua2,size2)\n",
    "    train_va2 = train2['va'].values\n",
    "    train_va2=split_month(train_va2,size2)\n",
    "    train_label2 = label2['nino'].values\n",
    "    train_label2=split_month_label(train_label2,size2)\n",
    "\n",
    "    print('Train samples: {}, Valid samples: {}'.format(len(train_label), len(train_label2)))\n",
    "\n",
    "    dict_train = {\n",
    "        'sst':train_sst,\n",
    "        't300':train_t300,\n",
    "        'ua':train_ua,\n",
    "        'va': train_va,\n",
    "        'label': train_label}\n",
    "    dict_valid = {\n",
    "        'sst':train_sst2,\n",
    "        't300':train_t3002,\n",
    "        'ua':train_ua2,\n",
    "        'va': train_va2,\n",
    "        'label': train_label2}\n",
    "    train_dataset = EarthDataSet(dict_train)\n",
    "    valid_dataset = EarthDataSet(dict_valid)\n",
    "    return train_dataset, valid_dataset\n",
    "\n",
    "class EarthDataSet(Dataset):\n",
    "    def __init__(self, data):\n",
    "        self.data = data\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.data['sst'])\n",
    "\n",
    "    def __getitem__(self, idx):   \n",
    "        return (self.data['sst'][idx], self.data['t300'][idx], self.data['ua'][idx], self.data['va'][idx]), self.data['label'][idx]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [],
   "source": [
    "class simpleSpatailTimeNN(nn.Module):\n",
    "    def __init__(self, n_cnn_layer:int=1, kernals:list=[3,3], n_lstm_units:int=12):\n",
    "        super(simpleSpatailTimeNN, self).__init__()\n",
    "        self.conv1 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12, kernel_size=3),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12, kernel_size=(3,4)),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,4))]) \n",
    "        self.conv2 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12, kernel_size=3),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12, kernel_size=(3,4)),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,4))])\n",
    "        self.conv3 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12, kernel_size=3),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12, kernel_size=(3,4)),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,4))])\n",
    "        self.conv4 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12, kernel_size=3),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12, kernel_size=(3,4)),\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,4))])\n",
    "        self.pool1 = nn.AdaptiveAvgPool2d((22, 1))\n",
    "        self.pool2 = nn.AdaptiveAvgPool2d((1, 70))\n",
    "        self.pool3 = nn.AdaptiveAvgPool2d((1, 128))\n",
    "        self.pool4 = nn.AdaptiveAvgPool2d((1, 128))\n",
    "        self.batch_norm = nn.BatchNorm1d(12, affine=False)\n",
    "        self.lstm = nn.LSTM(3*8*4,n_lstm_units, 2, bidirectional=True)\n",
    "        self.linear = nn.Linear(12*24,24)\n",
    "\n",
    "    def forward(self, sst, t300, ua, va):\n",
    "        for conv1 in self.conv1:\n",
    "            sst = conv1(sst)  # batch * 12 * ((24-2)/2-3)/3 * ((72 -2)/2-3)/4\n",
    "        for conv2 in self.conv2:\n",
    "            t300 = conv2(t300)\n",
    "        for conv3 in self.conv3:\n",
    "            ua = conv3(ua)\n",
    "        for conv4 in self.conv4:\n",
    "            va = conv4(va)\n",
    "\n",
    "        sst = torch.flatten(sst, start_dim=2)  # batch * 12 * 1540\n",
    "        t300 = torch.flatten(t300, start_dim=2)\n",
    "        ua = torch.flatten(ua, start_dim=2)\n",
    "        va = torch.flatten(va, start_dim=2)  # if flat, lstm input_dims = 24* 4              \n",
    "        x = torch.cat([sst, t300, ua, va], dim=-1) #在内层合并 batch*12*24*4\n",
    "        x = self.batch_norm(x)\n",
    "        x, _ = self.lstm(x)#输入24*4,n_lstm_units hidden,2layer且双向 输出batch*(channel*n_lstm_units*2)\n",
    "        #x = self.pool4(x).squeeze(dim=-2)\n",
    "        x=x.reshape((-1,12*24))\n",
    "        x = self.linear(x)#n_lstm_units*2-->24\n",
    "        return x\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#模型\n",
    "\n",
    "class simpleSpatailTimeNN(nn.Module):\n",
    "    def __init__(self, n_cnn_layer:int=1,n_lstm_units:int=64):\n",
    "        super(simpleSpatailTimeNN, self).__init__()\n",
    "        self.conv1 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12,kernel_size=3),#22*70\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),#11*35\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12,kernel_size=3),#9*33\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,3))] ),#3*11 \n",
    "        self.conv2 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12,kernel_size=3),#22*70\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),#11*35\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12,kernel_size=3),#9*33\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,3))] ),#3*11 \n",
    "        self.conv4 = nn.ModuleList([nn.Conv2d(in_channels=12, out_channels=12,kernel_size=3),#22*70\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(2,2)),#11*35\n",
    "                                    nn.Conv2d(in_channels=12, out_channels=12,kernel_size=3),#9*33\n",
    "                                    nn.ReLU(inplace = True),\n",
    "                                    nn.AvgPool2d(kernel_size=(3,3))] ),#3*11 \n",
    "        self.pool1 = nn.AdaptiveAvgPool2d((22, 1))\n",
    "        self.pool2 = nn.AdaptiveAvgPool2d((1, 70))\n",
    "        self.pool3 = nn.AdaptiveAvgPool2d((1, 128))\n",
    "        self.batch_norm = nn.BatchNorm1d(12, affine=False)\n",
    "        self.lstm = nn.LSTM(33 * 4, n_lstm_units, 2, bidirectional=True)\n",
    "        self.linear = nn.Linear(128, 24)\n",
    "\n",
    "    def forward(self, sst, t300, ua, va):\n",
    "        for conv1 in self.conv1:\n",
    "            sst = conv1(sst)  # batch * 12 * (3*11)\n",
    "        for conv2 in self.conv2:\n",
    "            t300 = conv2(t300)\n",
    "        for conv3 in self.conv3:\n",
    "            ua = conv3(ua)\n",
    "        for conv4 in self.conv4:\n",
    "            va = conv4(va)\n",
    "\n",
    "        sst = torch.flatten(sst, start_dim=2)  # batch *33\n",
    "        t300 = torch.flatten(t300, start_dim=2)\n",
    "        ua = torch.flatten(ua, start_dim=2)\n",
    "        va = torch.flatten(va, start_dim=2)  # if flat, lstm input_dims = 1540 * 4              \n",
    "            \n",
    "        x = torch.cat([sst, t300, ua, va], dim=-1) #在内层合并 batch*12*33*4\n",
    "        x = self.batch_norm(x)\n",
    "        x, _ = self.lstm(x)#输入33*4,64hidden,2layer且双向 输出batch*(128=64*2)\n",
    "        x = self.pool3(x).squeeze(dim=-2)\n",
    "        x = self.linear(x)#128-->24\n",
    "        return x\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "#loss function\n",
    "def coreff(x, y):\n",
    "    x_mean = np.mean(x)\n",
    "    y_mean = np.mean(y)\n",
    "    c1 = sum((x - x_mean) * (y - y_mean))\n",
    "    c2 = sum((x - x_mean)**2) * sum((y - y_mean)**2)\n",
    "    return c1/np.sqrt(c2)\n",
    "\n",
    "def rmse(preds, y):\n",
    "    return np.sqrt(sum((preds - y)**2)/preds.shape[0])\n",
    "\n",
    "def eval_score(preds, label):\n",
    "    # preds = preds.cpu().detach().numpy().squeeze()\n",
    "    # label = label.cpu().detach().numpy().squeeze()\n",
    "    acskill = 0\n",
    "    RMSE = 0\n",
    "    a = 0\n",
    "    a = [1.5]*4 + [2]*7 + [3]*7 + [4]*6\n",
    "    for i in range(24):\n",
    "        RMSE += rmse(label[:, i], preds[:, i])\n",
    "        cor = coreff(label[:, i], preds[:, i])\n",
    "    \n",
    "        acskill += a[i] * np.log(i+1) * cor\n",
    "    return 2/3 * acskill - RMSE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "#train\n",
    "fit_params = {\n",
    "    'n_epochs' : 50,\n",
    "    'learning_rate' : 1e-4,\n",
    "    'batch_size' : 128,\n",
    "}\n",
    "\n",
    "\n",
    "def train():\n",
    "    #set_seed()\n",
    "    train_dataset, valid_dataset = load_data2()      \n",
    "    train_loader = DataLoader(train_dataset, batch_size=fit_params['batch_size'])\n",
    "    valid_loader = DataLoader(valid_dataset, batch_size=fit_params['batch_size'])\n",
    "\n",
    "    model = simpleSpatailTimeNN()\n",
    "    device = 'cuda' if torch.cuda.is_available() else 'cpu'   \n",
    "    optimizer = torch.optim.Adam(model.parameters(), lr=fit_params['learning_rate'])\n",
    "    loss_fn = nn.MSELoss()   \n",
    "    \n",
    "    model.to(device)\n",
    "    loss_fn.to(device)\n",
    "\n",
    "    for i in range(fit_params['n_epochs']):\n",
    "        model.train()\n",
    "        for step, ((sst, t300, ua, va), label) in enumerate(train_loader):                \n",
    "            sst = sst.to(device).float()\n",
    "            t300 = t300.to(device).float()\n",
    "            ua = ua.to(device).float()\n",
    "            va = va.to(device).float()\n",
    "            optimizer.zero_grad()\n",
    "            label = label.to(device).float()\n",
    "            preds = model(sst, t300, ua, va)\n",
    "            loss = loss_fn(preds, label)\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            #print('Step: {}, Train Loss: {}'.format(step, loss))\n",
    "\n",
    "        model.eval()\n",
    "        y_true, y_pred = [], []\n",
    "        for step, ((sst, t300, ua, va), label) in enumerate(valid_loader):\n",
    "            sst = sst.to(device).float()\n",
    "            t300 = t300.to(device).float()\n",
    "            ua = ua.to(device).float()\n",
    "            va = va.to(device).float()\n",
    "            label = label.to(device).float()\n",
    "            preds = model(sst, t300, ua, va)\n",
    "            y_pred.append(preds)\n",
    "            y_true.append(label)\n",
    "\n",
    "        y_true = torch.cat(y_true, axis=0)\n",
    "        y_pred = torch.cat(y_pred, axis=0)\n",
    "        sco = eval_score(y_true.cpu().detach().numpy(), y_pred.cpu().detach().numpy())\n",
    "        print('Epoch: {}, Valid Score {}'.format(i+1,sco))\n",
    "\n",
    "        # torch.save(self.model.state_dict(), '../user_data/ref.pkl')\n",
    "        #torch.save(model, '../notebook/score22_original')\n",
    "        print('Model saved successfully')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#删除缺失值\n",
    "#新结构:先CNN卷四个分量kernel=5(20*68),然后avg pool 再卷积(stride=(5,4)-->4*17)再avg pool 再卷积(2*5),flatten再加月份 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Train samples: 11960, Valid samples: 1160\n",
      "Epoch: 1, Valid Score -13.375528539341305\n",
      "Model saved successfully\n",
      "Epoch: 2, Valid Score -11.287431564420652\n",
      "Model saved successfully\n",
      "Epoch: 3, Valid Score -8.449860967989768\n",
      "Model saved successfully\n",
      "Epoch: 4, Valid Score -4.74958548405991\n",
      "Model saved successfully\n",
      "Epoch: 5, Valid Score 0.6680813314618597\n",
      "Model saved successfully\n",
      "Epoch: 6, Valid Score 7.648673067627982\n",
      "Model saved successfully\n",
      "Epoch: 7, Valid Score 15.218538423282315\n",
      "Model saved successfully\n",
      "Epoch: 8, Valid Score 21.70805068701306\n",
      "Model saved successfully\n",
      "Epoch: 9, Valid Score 26.938827032640482\n",
      "Model saved successfully\n",
      "Epoch: 10, Valid Score 31.108747150477342\n",
      "Model saved successfully\n",
      "Epoch: 11, Valid Score 34.40141154816439\n",
      "Model saved successfully\n",
      "Epoch: 12, Valid Score 37.170495620849174\n",
      "Model saved successfully\n",
      "Epoch: 13, Valid Score 39.98491401799367\n",
      "Model saved successfully\n",
      "Epoch: 14, Valid Score 42.715558847524406\n",
      "Model saved successfully\n",
      "Epoch: 15, Valid Score 44.60022650955758\n",
      "Model saved successfully\n",
      "Epoch: 16, Valid Score 46.57967342931383\n",
      "Model saved successfully\n",
      "Epoch: 17, Valid Score 47.85192367972821\n",
      "Model saved successfully\n",
      "Epoch: 18, Valid Score 48.75047941645518\n",
      "Model saved successfully\n",
      "Epoch: 19, Valid Score 49.28429378339783\n",
      "Model saved successfully\n",
      "Epoch: 20, Valid Score 49.84948564044117\n",
      "Model saved successfully\n",
      "Epoch: 21, Valid Score 51.19099498775069\n",
      "Model saved successfully\n",
      "Epoch: 22, Valid Score 51.12722880793772\n",
      "Model saved successfully\n",
      "Epoch: 23, Valid Score 51.07314663694882\n",
      "Model saved successfully\n",
      "Epoch: 24, Valid Score 51.52723267101389\n",
      "Model saved successfully\n",
      "Epoch: 25, Valid Score 51.86622063355995\n",
      "Model saved successfully\n",
      "Epoch: 26, Valid Score 51.43916450403163\n",
      "Model saved successfully\n",
      "Epoch: 27, Valid Score 51.82043134395885\n",
      "Model saved successfully\n",
      "Epoch: 28, Valid Score 51.49995166547893\n",
      "Model saved successfully\n",
      "Epoch: 29, Valid Score 51.61019264097843\n",
      "Model saved successfully\n",
      "Epoch: 30, Valid Score 51.509226115257235\n",
      "Model saved successfully\n",
      "Epoch: 31, Valid Score 50.960328447321324\n",
      "Model saved successfully\n",
      "Epoch: 32, Valid Score 51.30003329427419\n",
      "Model saved successfully\n",
      "Epoch: 33, Valid Score 51.19876659152971\n",
      "Model saved successfully\n",
      "Epoch: 34, Valid Score 50.23600619239623\n",
      "Model saved successfully\n",
      "Epoch: 35, Valid Score 50.52779411311441\n",
      "Model saved successfully\n",
      "Epoch: 36, Valid Score 50.10001319534192\n",
      "Model saved successfully\n",
      "Epoch: 37, Valid Score 50.027908874315735\n",
      "Model saved successfully\n",
      "Epoch: 38, Valid Score 50.071778772671244\n",
      "Model saved successfully\n",
      "Epoch: 39, Valid Score 50.46094587090191\n",
      "Model saved successfully\n",
      "Epoch: 40, Valid Score 50.35655470698938\n",
      "Model saved successfully\n",
      "Epoch: 41, Valid Score 50.85345149873841\n",
      "Model saved successfully\n",
      "Epoch: 42, Valid Score 50.01770675496236\n",
      "Model saved successfully\n",
      "Epoch: 43, Valid Score 50.15972752902982\n",
      "Model saved successfully\n",
      "Epoch: 44, Valid Score 50.04467494224515\n",
      "Model saved successfully\n",
      "Epoch: 45, Valid Score 49.87277319709706\n",
      "Model saved successfully\n",
      "Epoch: 46, Valid Score 49.59726512544458\n",
      "Model saved successfully\n",
      "Epoch: 47, Valid Score 49.721176039155246\n",
      "Model saved successfully\n",
      "Epoch: 48, Valid Score 50.21432500561781\n",
      "Model saved successfully\n",
      "Epoch: 49, Valid Score 49.87364832524836\n",
      "Model saved successfully\n",
      "Epoch: 50, Valid Score 49.62440310482592\n",
      "Model saved successfully\n"
     ]
    }
   ],
   "source": [
    "train()\n",
    "#details:1.用CMIP的值训练SODA,训练出前12个月[sst,t300,ua,va]预测后24个月nino的模型\n",
    "#        2.结构:[sst,t300.ua,va]按照12个月分别过3*3卷积层(4channel,12*(24-2)*(72-2)),再flatten经纬度(4 features,12*1540)，最内层合并4个feature:(12*1540*4) 过batchnorm ,lstm：双向两层，输出batch*12*(64*2)  对12个月hidden平均池化，得到batch*128,全连接到24个月\n",
    "#problems:1.缺失值填0 2.优化函数是MSE 3.没有利用前12个月的nino\n",
    "#优化方向:需要突出空间经纬度相关性而不是直接flatten，更改网络结构，区分CMIP和SODA,ua va取滑动平均"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "pytorch"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
